Rust用Web framework、Rocketを使ってみよう
Introduction
Rust用のWebフレームワークはActix-webやaxumなどいろいろありますが、
それらと同じくらい人気のフレームワークが、Rocketです。
今回はこのRocketを試してみます。
Rocket?
Rocketは、使いやすさを維持しつつ、高速&安全&スケーラブルな
Webアプリを開発できるRust用Webフレームワークです。
シンプルなでAPIで直感的に使いやすく、
ドキュメントもそろっているので安心です。
テンプレート機能やルーティング、ミドルウェアなどの機能も持っていて、
基本的なWebアプリに必要な機能に加えて拡張性があります。
このあと実際にコードをかきつつ、Rocketの機能について紹介していきます。
Environment
今回試した環境は以下のとおりです。
- MacBook Pro (13-inch, M1, 2020)
- OS : MacOS 12.4
- Rust : 1.66.1
- cargo-shuttle : 0.10.0
cargo shuttleは下記コマンドでインストール可能です。
% cargo install cargo-shuttle
Try Rocket!
Quick start
では、CargoをつかってRustのプロジェクトを作成しましょう。
% cargo new rocket_example --bin Created binary (application) `rocket_example` package % cd rocket_example/
Cargo.tomlにrocketのライブラリを指定します。
rocket_dyn_templatesもあとで使うのでついでに設定しておきます。
[dependencies] rocket = "0.5.0-rc.2" [dependencies.rocket_dyn_templates] version = "0.1.0-rc.2" features = ["handlebars","tera"]
src/main.rsにHello worldプログラムを記述しましょう。
コードみるとわかるように、ルーティングの書き方もシンプルです。
マクロをつかってGETメソッドで/helloにアクセスすると
index関数が実行されます。
(マクロを使わなくても書ける)
use rocket::{get, launch, routes}; #[get("/hello")] fn index() -> &'static str { "Hello, world!" } #[launch] fn rocket() -> _ { rocket::build().mount("/", routes![index]) }
起動してアクセスしてみます。
% cargo run ・・・ Configured for debug. >> address: 127.0.0.1 >> port: 8000 ・・・ ? Routes: >> (index) GET /hello ? Fairings: >> Shield (liftoff, response, singleton) ?️ Shield: >> Permissions-Policy: interest-cohort=() >> X-Content-Type-Options: nosniff >> X-Frame-Options: SAMEORIGIN ? Rocket has launched from http://127.0.0.1:8000
http://localhost:8000/helloにGETでアクセスすると結果がかえってきます。
では、このあとRocketの機能をいくつか実装してみます。
テンプレート
Rocketはテンプレート機能を実装しており、handlebarsかteraを使って
簡単にテンプレートを作成することができます。
ここではteraをつかってみましょう。
まずは適当なteraファイル、
templates/index.html.teraファイルを作成します。
<!DOCTYPE html> <html> <head> <title>Templating Example</title> </head> <body> Argument :{{ foo }} </body> </html>
Rocketは設定項目としてはtemplate_dirをもっており、
デフォルトがtemplatesとなっています。
テンプレート用ディレクトリを変更したい場合は自分で
template_dirを設定しましょう。
main.rsを下記のように修正します。
use rocket::{get, launch, routes}; use rocket_dyn_templates::{Template, handlebars, context}; #[get("/templeting/<arg_foo>")] pub fn templating(arg_foo: &str) -> Template { Template::render("index", context! { foo: arg_foo, }) } #[launch] fn rocket() -> _ { rocket::build() .mount("/", routes![templating]) .attach(Template::fairing()) }
attach関数にTemplateを渡して、テンプレート機能を有効化します。
テンプレートは後述するFairingとして実装されているので、
これでテンプレート機能を使えます。
curlで動作確認。
% curl http://localhost:8000/templeting/tera-template <!DOCTYPE html> <html> <head> <title>Templating Example</title> </head> <body> Argument :tera-template </body> </html>%
なお、debugモードで起動している場合、
teraファイル修正後、サーバを再起動しなくても変更が反映されます。
Profiles
debug用、release用などのモードに応じたアプリ設定を簡単に行うことができます。
プロジェクトルートにRocket.tomlファイルを下記内容で作成しましょう。
[default] host = "localhost" limits = { form = "64 kB", json = "1 MiB" } foo="bar" [debug] port = 8000 limits = { json = "10MiB" } foo="buzz" [release] host="dev.classmethod.jp" port = 9999 foo="hoge"
main.rsでConfigをつかってみます。
Rocket起動時、Rocket.tomlの設定が
起動したときのモード(--debugとか--releseとか)
に応じてロードされ、コンソールに表示されます。
use rocket::serde::Deserialize; use rocket::{get, launch, routes}; ・・・ #[launch] fn rocket() -> _ { let rocket = rocket::build() .mount("/", routes![index]); let figment = rocket.figment(); #[derive(Deserialize,Debug)] #[serde(crate = "rocket::serde")] struct Config { port: u16, foo: String, //var_env:String, } let config: Config = figment.extract().expect("config"); println!("{:?}",config); rocket }
cargo runで起動すると、デフォルトはdebugモードなので
下記内容がコンソールに出力されます。
・・・ Config { port: 8000, foo: "buzz" } ・・・
また、prefixに「ROCKET_」とつけた環境変数があれば、その値も設定されます。
(Rocket.tomlの値より優先される)
つまり、↓のように「ROCKET_VAR_ENV」という環境変数をセットすれば、
var_envという名前のパラメータが使えます。
% export ROCKET_VAR_ENV="hello"
Testing Library
Rocketのユニットテスト&結合テストは、標準のテスト用ライブラリを使って簡単にかけます。
↓のindex関数をテストしたい場合、
//main.rs #[get("/hello")] fn index() -> &'static str { "Hello, world!" }
下記のようにClientオブジェクトを使って、
簡単に検証可能です。
//main.rs #[cfg(test)] mod test { use super::rocket; use rocket::uri; use rocket::http::{ContentType, Status}; use rocket::local::blocking::Client; #[test] fn index() { let client = Client::tracked(rocket()).expect("valid rocket instance"); let mut response = client.get(uri!(super::index)).dispatch(); assert_eq!(response.status(), Status::Ok); assert_eq!(response.content_type(), Some(ContentType::Plain)); assert!(response.headers().get_one("X-Content-Type-Options").is_some()); assert_eq!(response.into_string().unwrap(), "Hello, world!"); } }
cargo testでテストが実行されます。
% cargo test running 1 tests test test::index ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
Fairings
さきほどteraテンプレートをつかった機能は、
FairingsというRocketの機能を使って実現しています。
これは、Expressのミドルウェアみたいなもので、
通常のリクエストーレスポンス間に任意の処理を実装する仕組みです。
ここにあるサンプルをつかってみましょう。
src/fairings.rsファイルを↓のように実装します。
(fairing関数以外はサンプルそのまま)
use std::future::Future; use std::io::Cursor; use std::pin::Pin; use std::sync::atomic::{AtomicUsize, Ordering}; use rocket::{Request, Data, Response}; use rocket::fairing::{Fairing, Info, Kind}; use rocket::http::{Method, ContentType, Status}; #[derive(Default)] pub struct Counter { get: AtomicUsize, post: AtomicUsize, } impl Counter { pub fn fairing() -> impl Fairing { Counter{ get:AtomicUsize::new(0), post:AtomicUsize::new(0) } } } #[rocket::async_trait] impl Fairing for Counter { fn info(&self) -> Info { Info { name: "GET/POST Counter", kind: Kind::Request | Kind::Response } } async fn on_request(&self, req: &mut Request<'_>, _: &mut Data<'_>) { if req.method() == Method::Get { self.get.fetch_add(1, Ordering::Relaxed); } else if req.method() == Method::Post { self.post.fetch_add(1, Ordering::Relaxed); } } async fn on_response<'r>(&self, req: &'r Request<'_>, res: &mut Response<'r>) { // Don't change a successful user's response, ever. if res.status() != Status::NotFound { return } if req.method() == Method::Get && req.uri().path() == "/counts" { let get_count = self.get.load(Ordering::Relaxed); let post_count = self.post.load(Ordering::Relaxed); let body = format!("Get: {}\nPost: {}", get_count, post_count); res.set_status(Status::Ok); res.set_header(ContentType::Plain); res.set_sized_body(body.len(), Cursor::new(body)); } } }
Fairingを作るには、rocket::fairing::Fairingトレイトを実装します。
Fairingはリクエスト/レスポンスなどのイベントのコールバックを受け取り、
リクエスト/レスポンスの書き換えやロギングなど、
自由に処理させることができます。
次に、src/main.rsを修正します。
build時に↑のFairingsをattachします。
・・・ mod fairings; use fairings::*; use rocket::fairing::AdHoc; #[launch] fn rocket() -> _ { rocket::build() .mount("/", routes![index]) .attach(Counter::fairing()) //Counter fairingsを追加 .attach(Template::fairing()) }
アプリを起動後、何度かindexをアクセスします。
その後/countsにアクセスすると、Counter fairingによって計測された
Get/Postリクエスト実行回数を表示します。
% curl http://localhost:8000/counts Get: 12 Post: 0
また、Fairingトレイトを実装するのが面倒な場合は
AdHoc Fairingを使う方法もあります。
これは、rocket::fairing::AdHocの関数とクロージャをつかって
簡単に実装できます。
↓では、3つのAdHoc Fairing設定しています。
on_liftoffは起動時、on_requestはすべてのリクエスト時、
on_shutdownはアプリのシャットダウン前にそれぞれ実行されます。
use rocket::{get, launch, routes}; use rocket::fairing::AdHoc; ・・・ #[launch] fn rocket() -> _ { rocket::build() .mount("/", routes![index]) .attach(AdHoc::on_liftoff("Startup", |_| Box::pin(async move { println!("=== start up! ==="); }))) .attach(AdHoc::on_request("All Request", |req, _| Box::pin(async move { println!("{:?}",req); }))) .attach(AdHoc::on_shutdown("Shutdow", |_| Box::pin(async move { println!("=== shutdown! ==="); }))) }
以上、Rocketの機能をいくつか簡単に解説しました。
まだまだ便利な機能がありますので、ガイドをご確認ください。
Rocket with Shuttle
以前紹介した、
RustのサーバレスプラットフォームであるShuttle.rsでは、Rocketを選択して
アプリを実装することができます。
手順は簡単で、cargo-shuttleをつかって
init時にRocketを選択すればOKです。
% mkdir your_shuttle_project && cd your_shuttle_project % cargo shuttle login #APIキーを入力 % cargo shuttle init How do you want to name your project? It will be hosted at ${project_name}.shuttleapp.rs. ✔ Project name · <your_shuttle_project> Where should we create this project? ✔ Directory · . Shuttle works with a range of web frameworks. Which one do you want to use? › actix-web axum ❯ rocket tide tower poem salvo serenity poise warp thruster none
runでローカル起動、deployでShuttleにデプロイします。
% cargo shuttle run % cargo shuttle deploy
Summary
今回はRustのWebフレームワーク、Rocketを試してみました。
とてもシンプルで使いやすいですね。
Shuttle.rsでも使えますし、今後も期待です。